#include "mylib.h"
#include "mylog.h"

#include <dirent.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/*
        遍历 /proc/ 下进程的cmdline来确定进程号
        返回0 表示没有找到，返回负数表示出错,、
        返回正数表示找到的pid的个数
        如如果pid == NULL，则不会存储获取的pid，但仍然会返回找到了多少个符合要求的pid

        keywords 越是详细，找到的越精准，否则，可能会找到多个
        如果直接找目标，可以传递 cmdline 给 keywords[0] 给来查询

        keywords 是字符串数组，需要按顺序填写关键字，至少要有1个关键字
*/
int mylib_pid_search(const char *keywords[], int keynmemb, pid_t *pid, int pidnmemb)
{
        errno_t err = 0;
        DIR *dir = NULL;
        struct dirent *pdir = NULL;
        FILE *file = NULL;
        char path[256] = {0};
        char cache[1024 * 10] = {0};
        int nbytes = 0;
        int cursor = 0;
        const char *p = NULL;
        bool is_target = false;

        if (keywords == NULL) {
                err = -EFAULT;
                MYLOG("keywords == NULL\n");
                goto err0;
        }

        if (keynmemb < 1) {
                err = -EINVAL;
                MYLOG("keynmemb < 1\n");
                goto err0;
        }

        if (strnlen(keywords[0], 1024) == 0) {
                err = -EINVAL;
                MYLOG("strnlen(keywords[0], 1024) == 0\n");
                goto err0;
        }

        dir = opendir("/proc");
        if (dir == NULL) {
                err = -errno;
                MYLOG("opendir(\"/proc\") err, %s\n", strerror(errno));
                goto err0;
        }

        // readdir 大都是线程安全的，readdir_r未来可能被废弃
        while ((pdir = readdir(dir)) != NULL) {
                if (strcmp(pdir->d_name, ".") == 0) {
                        // 不是进程文件
                        continue;
                }

                if (strcmp(pdir->d_name, "..") == 0) {
                        // 不是进程文件
                        continue;
                }

                if (pdir->d_type != DT_DIR) {
                        // DT_DIR -->  A directory
                        // 进程信息存储在目录文件中
                        continue;
                }

                nbytes = snprintf(path, sizeof(path), "/proc/%s/cmdline", pdir->d_name);
                if (nbytes < 0) {
                        err = -errno;
                        MYLOG("snprintf() err, %s\n", strerror(errno));
                        goto err1;
                } else if (nbytes >= sizeof(path)) {
                        err = -1;
                        MYLOG("path too small, need %d bytes\n", nbytes);
                        goto err1;
                }

                file = fopen(path, "r");
                if (file == NULL) {
                        if (errno == ENOENT) {
                                // 有的进程不存在cmdline
                                continue;
                        }
                        err = -errno;
                        MYLOG("fopen(%s) err, %s\n", path, strerror(errno));
                        goto err1;
                }

                memset(cache, 0, sizeof(cache));
                nbytes = fread(cache, 1, sizeof(cache), file);
                fclose(file);
                if (nbytes < 0) {
                        err = -errno;
                        MYLOG("fread(%s) err, %s\n", path, strerror(errno));
                        goto err1;
                }

                // 发现 kill 后 /proc/ 中仍然存在其文件夹，但是cmdline中没有内容
                if (nbytes == 0) {
                        err = -ENOENT;
                        // 可能是僵尸进程，也可能不是
                        // MYLOG("%s is NULL\n", path);
                        continue;
                }

                // /proc/*/cmdline 中的字符串，会将空格变成 \0 ，这里需要将其还原成空格
                // 最后的\0 不需要变换成空格
                for (int i = 0, imax = nbytes - 1; i < imax; ++i) {
                        if (cache[i] == '\0')
                                cache[i] = ' ';
                }

                is_target = true;
                p = cache;
                for (int i = 0; i < keynmemb; ++i) {
                        p = strstr(p, keywords[i]);
                        if (p == NULL) {
                                // 不是目标
                                is_target = false;
                                break;
                        }
                }
                if (is_target == false) {
                        continue;
                }

                if (pid && cursor < pidnmemb) {
                        pid[cursor] = atoi(pdir->d_name);
                }
                ++cursor;
        }

        closedir(dir);

        return cursor;
err1:
        closedir(dir);
err0:
        return err;
}

/*
 fork fork 的方式来实现system()/popen()的功能,并能获取其pid（用于kill -9）
 非阻塞
 int *pid 子子进程的PID
*/
errno_t mylib_pid_exec(int *pid, const char *cmdline)
{
        errno_t err = 0;
        pid_t child_pid = 0;
        int pipefd[2] = {0};

        if (pipe(pipefd) < 0) {
                err = -errno;
                MYLOG("pipe() err = %d\n", err);
                goto err0;
        }

        child_pid = fork();
        if (child_pid < 0) {
                err = -errno;
                MYLOG("fork() err, %s\n", strerror(errno));

                close(pipefd[0]);
                close(pipefd[1]);

                goto err0;
        } else if (child_pid == 0) {
                // 子进程
                // 子进程，里面的变量已经和父进程脱离了，不能直接赋值给父进程

                pid_t child_child_pid = 0;

                child_child_pid = fork();
                if (child_child_pid < 0) {
                        err = -errno;
                        MYLOG("fork() err, %s\n", strerror(errno));

                        close(pipefd[0]);
                        close(pipefd[1]);

                } else if (child_child_pid > 0) {
                        // 父进程，退出，使其子进程变成孤儿进程

                        write(pipefd[1], &child_child_pid,
                              sizeof(child_child_pid)); // 阻塞，直到pipe中的数据被读取

                        MYLOG("i am child father %d, exit\r\n", getpid());

                        close(pipefd[0]);
                        close(pipefd[1]);

                        exit(0);
                } else {
                        // 孤儿进程
                        MYLOG("i am child child %d, exec\r\n", getpid());

                        close(pipefd[0]);
                        close(pipefd[1]);

                        setpgid(child_child_pid, child_child_pid); // kill 此子进程后，子进程创建的相关进程也会kill
                        execl("/bin/sh", "sh", "-c", cmdline, (char *) 0);
                        _exit(127); //子进程正常执行则不会执行此语句， 注意 _exit 退出后，由内核接管并清理进程，这个是子进程需要的
                }
        } else {
                // 父进程

                pid_t child_father_pid = 0;
                pid_t child_child_pid = 0;

                read(pipefd[0], &child_child_pid, sizeof(child_child_pid));

                close(pipefd[0]);
                close(pipefd[1]);

                if (pid) {
                        *pid = child_child_pid;
                }

                child_father_pid = wait(NULL); // 等待第一层的子进程退出
                (void) child_father_pid;

                MYLOG("i am father %d, my child is %d exit, my child's child is %d "
                      "running\r\n",
                      getpid(), child_father_pid, child_child_pid);
        }

        return 0;
err0:
        return err;
}
